﻿/*******************************************************************************
 * Copyright (c) 2010-2011 Robert Fraser                                       *
 *                                                                             *
 * Permission is hereby granted, free of charge, to any person obtaining a     *
 * copy of this software and associated documentation files (the "Software"),  *
 * to deal in the Software without restriction, including without limitation   *
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,    *
 * and/or sell copies of the Software, and to permit persons to whom the       *
 * Software is furnished to do so, subject to the following conditions:        *
 *                                                                             *
 *   - The above copyright notice and this permission notice shall be included *
 *     in all copies or substantial portions of the Software.                  *
 *                                                                             *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  *
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,    *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER      *
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING     *
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER         *
 * DEALINGS IN THE SOFTWARE.                                                   *
 *******************************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using zeal.core;
using zeal.ui.util;
using zeal.util;

namespace zeal.ui.graph
{
    internal partial class GraphEditor
    {
        public static readonly RoutedUICommand layoutCommand = new RoutedUICommand("Layout", "Layout",
           typeof(GraphEditor), new InputGestureCollection { new KeyGesture(Key.L, ModifierKeys.Control) });

        private void initCommands()
        {
            this.bindCommand(ApplicationCommands.SelectAll, canExecuteAny, selectAll);
            this.bindCommand(ApplicationCommands.Delete, canCopy, doRemove);
            this.bindCommand(ApplicationCommands.Cut, canCopy, doCut);
            this.bindCommand(ApplicationCommands.Copy, canCopy, doCopy);
            this.bindCommand(ApplicationCommands.Paste, canPaste, doPaste);
            this.bindCommand(layoutCommand, canExecuteAny, doLayout);
        }

        private GraphEdge[] getInternalConnections(HashSet<GraphNode> nodes) { return edges.Where(e => nodes.Contains(e.input.node) && nodes.Contains(e.output.node)).ToArray(); }
        private GraphEdge[] getAllConnections(HashSet<GraphNode> nodes) { return edges.Where(e => nodes.Contains(e.input.node) || nodes.Contains(e.output.node)).ToArray(); }
        private static Action getAction<T1>(Action<T1> action, T1 p1) { return () => action(p1); }
        
        private bool canExecuteAny() { return isInDefaultMode; }
        private bool canCopy() { return canExecuteAny() && selectedNodes.Any(); }
        private bool canPaste() { return canExecuteAny() && _clipboard != null; }

        private void doCut() { doCopy(); doRemove(); }
        private void doPaste() { pushUndo(executePaste()); }
        private void doRemove() { pushUndo(executeRemove()); }
        
        private void doCopy()
        {
            HashSet<GraphNode> nodeSet = new HashSet<GraphNode>(selectedNodes);
            if(nodeSet.Count > 0) _clipboard = new ClipboardData(graph, nodeSet, getInternalConnections(nodeSet));
        }
        
        private void doLayout()
        {
            deselectAll();
            IMemento memento = executeLayout();
            if(null != memento)
            {
                pushUndo(memento);
                _viewer.OriginX = 0;
                _viewer.OriginY = 0;
            }
        }

        private IMemento executeAdd(TextureNodeInfo info, Point pos) { GraphNode node; return executeAdd(info, pos, out node); }
        private IMemento executeAdd(TextureNodeInfo info, Point pos, out GraphNode node)
        {
            node = new GraphNode(graph.add(info), this);
            Canvas.SetLeft(node, pos.X);
            Canvas.SetTop(node, pos.Y);
            surface.addChild(node);
            snapIfNeeded(node);
            _viewer.Focus();
            return new Memento(getAction(addNodeBackImpl, node), getAction(removeNodeImpl, node),
                  string.Format("Add {0} node", info.name));
        }

        private IMemento executeRemove()
        {
            HashSet<GraphNode> removedNodes = new HashSet<GraphNode>(selectedNodes);
            if (removedNodes.Count == 0)
                return null;
            GraphEdge[] removedEdges = getAllConnections(removedNodes);
            MultiMemento memento = new MultiMemento(removedNodes.Count == 1 ? "Remove " + removedNodes.First().node : "Remove " + removedNodes.Count + " nodes");
            foreach (GraphEdge edge in removedEdges)
                memento.add(executeDisconnect(edge));
            foreach (GraphNode node in removedNodes)
            {
                IMemento removeNode = new Memento(getAction(removeNodeImpl, node), getAction(addNodeBackImpl, node));
                removeNode.redo();
                memento.add(removeNode);
            }
            return memento;
        }

        private IMemento executeConnect(GraphPort port1, GraphPort port2)
        {
            GraphPort inPort = port1.isInput ? port1 : port2;
            GraphPort outPort = port1.isInput ? port2 : port1;
            if (_graph.areConnected((TextureInputPort)inPort.port, (TextureOutputPort)outPort.port))
                return null;
            GraphEdge edge = new GraphEdge(inPort, outPort);
            IMemento memento = new Memento(getAction(connectImpl, edge), getAction(disconnectImpl, edge),
                string.Format("Connect {0} and {1}", inPort.port, outPort.port));
            memento.redo();
            return memento;
        }

        private IMemento executeDisconnect(GraphEdge edge)
        {
            if (edge == null) throw new ArgumentNullException("edge");
            if (!_graph.areConnected((TextureInputPort)edge.input.port, (TextureOutputPort)edge.output.port))
                return null;
            IMemento memento = new Memento(getAction(disconnectImpl, edge), getAction(connectImpl, edge),
                string.Format("Disconnect {0} and {1}", edge.input.port, edge.output.port));
            memento.redo();
            return memento;
        }

        private IMemento executePaste()
        {
            ClipboardData data = _clipboard;
            if (null == data || data.nodes.Count == 0)
                return null;
            deselectAll();
            bool isSameCanvas = data.graph == graph;
            Dictionary<GraphPort, GraphPort> copiedPorts = new Dictionary<GraphPort, GraphPort>();
            MultiMemento memento = new MultiMemento(
                data.nodes.Count == 1 ? "Paste " + data.nodes.First().node : "Paste " + data.nodes.Count + " nodes");

            // Copy the nodes themselves
            foreach (GraphNode node in data.nodes)
            {
                GraphNode copy;
                Point pos = new Point(node.left, node.top);
                if (isSameCanvas) pos += new Vector(GRID_SIZE, GRID_SIZE);
                memento.add(executeAdd(node.node.info, pos, out copy));
                copy.IsSelected = true;
                foreach (GraphPort port in node.inPorts)
                {
                    GraphPort port1 = port;
                    copiedPorts.Add(port1, copy.inPorts.First(p => p.name == port1.name));
                }
                foreach (GraphPort port in node.outPorts)
                {
                    GraphPort port1 = port;
                    copiedPorts.Add(port1, copy.outPorts.First(p => p.name == port1.name));
                }
            }

            // Copy internally consistent connecting edges
            foreach (GraphEdge edge in data.edges)
            {
                GraphPort input = copiedPorts.getOrDefault(edge.input);
                GraphPort output = copiedPorts.getOrDefault(edge.output);
                Debug.Assert(input != null && output != null);
                memento.add(executeConnect(input, output));
            }

            return memento;
        }

        private IMemento executeLayout()
        {
            MoveMemento memento = new MoveMemento("Layout");
            foreach (GraphNode node in nodes)
                memento.old.Add(node, new Point(node.left, node.top));
            if (LayoutManager.layout(this))
            {
                foreach (GraphNode node in nodes)
                    memento.cur.Add(node, new Point(node.left, node.top));
                return memento;
            }
            return null;
        }

        private void addNodeBackImpl(GraphNode node)
        {
            _graph.add(node.node);
            surface.addChild(node);
            snapIfNeeded(node);
        }

        private void removeNodeImpl(GraphNode node)
        {
            if (node == null) throw new ArgumentNullException("node");
            Debug.Assert(!_graph.hasConnections(node.node), "Node is still connected to other nodes");
            node.IsSelected = false;
            bool success = _graph.remove(node.node);
            Debug.Assert(success);
            surface.removeChild(node);
        }

        private void connectImpl(GraphEdge edge)
        {
            if (edge == null) throw new ArgumentNullException("edge");
            bool success = _graph.connect((TextureInputPort)edge.input.port, (TextureOutputPort)edge.output.port);
            Debug.Assert(success);
            edge.input.addEdge(edge);
            edge.output.addEdge(edge);
            surface.addChild(edge);
        }

        private void disconnectImpl(GraphEdge edge)
        {
            if (edge == null) throw new ArgumentNullException("edge");
            bool success = _graph.disconnect((TextureInputPort)edge.input.port, (TextureOutputPort)edge.output.port);
            Debug.Assert(success);
            edge.input.removeEdge(edge);
            edge.output.removeEdge(edge);
            surface.removeChild(edge);
        }

        
        private sealed class MoveMemento : IMemento
        {
            public readonly Dictionary<GraphNode, Point> old = new Dictionary<GraphNode, Point>();
            public readonly Dictionary<GraphNode, Point> cur = new Dictionary<GraphNode, Point>();
            public string name { get; set; }
            public override string ToString() { return name; }
            public MoveMemento(string name) { this.name = name; }

            public void undo()
            {
                foreach (KeyValuePair<GraphNode, Point> kvp in old)
                {
                    kvp.Key.left = kvp.Value.X;
                    kvp.Key.top = kvp.Value.Y;
                }
            }

            public void redo()
            {
                foreach (KeyValuePair<GraphNode, Point> kvp in cur)
                {
                    kvp.Key.left = kvp.Value.X;
                    kvp.Key.top = kvp.Value.Y;
                }
            }
        }

        private sealed class ClipboardData
        {
            public readonly TextureGraph graph;
            public readonly HashSet<GraphNode> nodes;
            public readonly GraphEdge[] edges;

            public ClipboardData(TextureGraph graph, HashSet<GraphNode> nodes, GraphEdge[] edges)
            {
                this.graph = graph;
                this.nodes = nodes;
                this.edges = edges;
            }
        }
    }
}